define(function (require, exports) {

/**
 * plupload.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under GPL License.
 *
 * License: http://www.plupload.com/license
 * Contributing: http://www.plupload.com/contributing
 * 1.5.4
 */

// JSLint defined globals
/*global window:false, escape:false */

(function() {
    var count = 0, runtimes = [], i18n = {}, mimes = {},
        xmlEncodeChars = {'<' : 'lt', '>' : 'gt', '&' : 'amp', '"' : 'quot', '\'' : '#39'},
        xmlEncodeRegExp = /[<>&\"\']/g, undef, delay = window.setTimeout,
        // A place to store references to event handlers
        eventhash = {},
        uid;

    // IE W3C like event funcs
    function preventDefault() {
        this.returnValue = false;
    }

    function stopPropagation() {
        this.cancelBubble = true;
    }

    // Parses the default mime types string into a mimes lookup map
    (function(mime_data) {
        var items = mime_data.split(/,/), i, y, ext;

        for (i = 0; i < items.length; i += 2) {
            ext = items[i + 1].split(/ /);

            for (y = 0; y < ext.length; y++) {
                mimes[ext[y]] = items[i];
            }
        }
    })(
        "application/msword,doc dot," +
        "application/pdf,pdf," +
        "application/pgp-signature,pgp," +
        "application/postscript,ps ai eps," +
        "application/rtf,rtf," +
        "application/vnd.ms-excel,xls xlb," +
        "application/vnd.ms-powerpoint,ppt pps pot," +
        "application/zip,zip," +
        "application/x-shockwave-flash,swf swfl," +
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document,docx," +
        "application/vnd.openxmlformats-officedocument.wordprocessingml.template,dotx," +
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,xlsx," +
        "application/vnd.openxmlformats-officedocument.presentationml.presentation,pptx," + 
        "application/vnd.openxmlformats-officedocument.presentationml.template,potx," +
        "application/vnd.openxmlformats-officedocument.presentationml.slideshow,ppsx," +
        "application/x-javascript,js," +
        "application/json,json," +
        "audio/mpeg,mpga mpega mp2 mp3," +
        "audio/x-wav,wav," +
        "audio/mp4,m4a," +
        "image/bmp,bmp," +
        "image/gif,gif," +
        "image/jpeg,jpeg jpg jpe," +
        "image/photoshop,psd," +
        "image/png,png," +
        "image/svg+xml,svg svgz," +
        "image/tiff,tiff tif," +
        "text/plain,asc txt text diff log," +
        "text/html,htm html xhtml," +
        "text/css,css," +
        "text/csv,csv," +
        "text/rtf,rtf," +
        "video/mpeg,mpeg mpg mpe," +
        "video/quicktime,qt mov," +
        "video/mp4,mp4," +
        "video/x-m4v,m4v," +
        "video/x-flv,flv," +
        "video/x-ms-wmv,wmv," +
        "video/avi,avi," +
        "video/webm,webm," +
        "video/vnd.rn-realvideo,rv," +
        "application/vnd.oasis.opendocument.formula-template,otf," +
        "application/octet-stream,exe"
    );

    /**
     * Plupload class with some global constants and functions.
     *
     * @example
     * // Encode entities
     * console.log(plupload.xmlEncode("My string &lt;&gt;"));
     *
     * // Generate unique id
     * console.log(plupload.guid());
     *
     * @static
     * @class plupload
     */
    var plupload = {
        /**
         * Plupload version will be replaced on build.
         */
        VERSION : '1.5.4',

        /**
         * Inital state of the queue and also the state ones it's finished all it's uploads.
         *
         * @property STOPPED
         * @final
         */
        STOPPED : 1,

        /**
         * Upload process is running
         *
         * @property STARTED
         * @final
         */
        STARTED : 2,

        /**
         * File is queued for upload
         *
         * @property QUEUED
         * @final
         */
        QUEUED : 1,

        /**
         * File is being uploaded
         *
         * @property UPLOADING
         * @final
         */
        UPLOADING : 2,

        /**
         * File has failed to be uploaded
         *
         * @property FAILED
         * @final
         */
        FAILED : 4,

        /**
         * File has been uploaded successfully
         *
         * @property DONE
         * @final
         */
        DONE : 5,

        // Error constants used by the Error event

        /**
         * Generic error for example if an exception is thrown inside Silverlight.
         *
         * @property GENERIC_ERROR
         * @final
         */
        GENERIC_ERROR : -100,

        /**
         * HTTP transport error. For example if the server produces a HTTP status other than 200.
         *
         * @property HTTP_ERROR
         * @final
         */
        HTTP_ERROR : -200,

        /**
         * Generic I/O error. For exampe if it wasn't possible to open the file stream on local machine.
         *
         * @property IO_ERROR
         * @final
         */
        IO_ERROR : -300,

        /**
         * Generic I/O error. For exampe if it wasn't possible to open the file stream on local machine.
         *
         * @property SECURITY_ERROR
         * @final
         */
        SECURITY_ERROR : -400,

        /**
         * Initialization error. Will be triggered if no runtime was initialized.
         *
         * @property INIT_ERROR
         * @final
         */
        INIT_ERROR : -500,

        /**
         * File size error. If the user selects a file that is too large it will be blocked and an error of this type will be triggered.
         *
         * @property FILE_SIZE_ERROR
         * @final
         */
        FILE_SIZE_ERROR : -600,

        /**
         * File extension error. If the user selects a file that isn't valid according to the filters setting.
         *
         * @property FILE_EXTENSION_ERROR
         * @final
         */
        FILE_EXTENSION_ERROR : -601,
        
        /**
         * Runtime will try to detect if image is proper one. Otherwise will throw this error.
         *
         * @property IMAGE_FORMAT_ERROR
         * @final
         */
        IMAGE_FORMAT_ERROR : -700,
        
        /**
         * While working on the image runtime will try to detect if the operation may potentially run out of memeory and will throw this error.
         *
         * @property IMAGE_MEMORY_ERROR
         * @final
         */
        IMAGE_MEMORY_ERROR : -701,
        
        /**
         * Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error.
         *
         * @property IMAGE_DIMENSIONS_ERROR
         * @final
         */
        IMAGE_DIMENSIONS_ERROR : -702,
        

        /**
         * Mime type lookup table.
         *
         * @property mimeTypes
         * @type Object
         * @final
         */
        mimeTypes : mimes,
        
        /**
         * In some cases sniffing is the only way around :(
         */
        ua: (function() {
            var nav = navigator, userAgent = nav.userAgent, vendor = nav.vendor, webkit, opera, safari;
            
            webkit = /WebKit/.test(userAgent);
            safari = webkit && vendor.indexOf('Apple') !== -1;
            opera = window.opera && window.opera.buildNumber;
            
            return {
                windows: navigator.platform.indexOf('Win') !== -1,
                ie: !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName),
                webkit: webkit,
                gecko: !webkit && /Gecko/.test(userAgent),
                safari: safari,
                opera: !!opera
            };
        }()),
        
        /**
         * Gets the true type of the built-in object (better version of typeof).
         * @credits Angus Croll (http://javascriptweblog.wordpress.com/)
         *
         * @param {Object} o Object to check.
         * @return {String} Object [[Class]]
         */
        typeOf: function(o) {
            return ({}).toString.call(o).match(/\s([a-z|A-Z]+)/)[1].toLowerCase();
        },

        /**
         * Extends the specified object with another object.
         *
         * @method extend
         * @param {Object} target Object to extend.
         * @param {Object..} obj Multiple objects to extend with.
         * @return {Object} Same as target, the extended object.
         */
        extend : function(target) {
            plupload.each(arguments, function(arg, i) {
                if (i > 0) {
                    plupload.each(arg, function(value, key) {
                        target[key] = value;
                    });
                }
            });

            return target;
        },

        /**
         * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _.
         *
         * @method cleanName
         * @param {String} s String to clean up.
         * @return {String} Cleaned string.
         */
        cleanName : function(name) {
            var i, lookup;

            // Replace diacritics
            lookup = [
                /[\300-\306]/g, 'A', /[\340-\346]/g, 'a', 
                /\307/g, 'C', /\347/g, 'c',
                /[\310-\313]/g, 'E', /[\350-\353]/g, 'e',
                /[\314-\317]/g, 'I', /[\354-\357]/g, 'i',
                /\321/g, 'N', /\361/g, 'n',
                /[\322-\330]/g, 'O', /[\362-\370]/g, 'o',
                /[\331-\334]/g, 'U', /[\371-\374]/g, 'u'
            ];

            for (i = 0; i < lookup.length; i += 2) {
                name = name.replace(lookup[i], lookup[i + 1]);
            }

            // Replace whitespace
            name = name.replace(/\s+/g, '_');

            // Remove anything else
            name = name.replace(/[^a-z0-9_\-\.]+/gi, '');

            return name;
        },

        /**
         * Adds a specific upload runtime like for example flash or gears.
         *
         * @method addRuntime
         * @param {String} name Runtime name for example flash.
         * @param {Object} obj Object containing init/destroy method.
         */
        addRuntime : function(name, runtime) {          
            runtime.name = name;
            runtimes[name] = runtime;
            runtimes.push(runtime);

            return runtime;
        },

        /**
         * Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers.
         * The only way a user would be able to get the same ID is if the two persons at the same exact milisecond manages
         * to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique.
         * It's more probable for the earth to be hit with an ansteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property
         * to an user unique key.
         *
         * @method guid
         * @return {String} Virtually unique id.
         */
        guid : function() {
            var guid = new Date().getTime().toString(32), i;

            for (i = 0; i < 5; i++) {
                guid += Math.floor(Math.random() * 65535).toString(32);
            }

            return (plupload.guidPrefix || 'p') + guid + (count++).toString(32);
        },

        /**
         * Builds a full url out of a base URL and an object with items to append as query string items.
         *
         * @param {String} url Base URL to append query string items to.
         * @param {Object} items Name/value object to serialize as a querystring.
         * @return {String} String with url + serialized query string items.
         */
        buildUrl : function(url, items) {
            var query = '';

            plupload.each(items, function(value, name) {
                query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value);
            });

            if (query) {
                url += (url.indexOf('?') > 0 ? '&' : '?') + query;
            }

            return url;
        },

        /**
         * Executes the callback function for each item in array/object. If you return false in the
         * callback it will break the loop.
         *
         * @param {Object} obj Object to iterate.
         * @param {function} callback Callback function to execute for each item.
         */
        each : function(obj, callback) {
            var length, key, i;

            if (obj) {
                length = obj.length;

                if (length === undef) {
                    // Loop object items
                    for (key in obj) {
                        if (obj.hasOwnProperty(key)) {
                            if (callback(obj[key], key) === false) {
                                return;
                            }
                        }
                    }
                } else {
                    // Loop array items
                    for (i = 0; i < length; i++) {
                        if (callback(obj[i], i) === false) {
                            return;
                        }
                    }
                }
            }
        },

        /**
         * Formats the specified number as a size string for example 1024 becomes 1 KB.
         *
         * @method formatSize
         * @param {Number} size Size to format as string.
         * @return {String} Formatted size string.
         */
        formatSize : function(size) {
            if (size === undef || /\D/.test(size)) {
                return plupload.translate('N/A');
            }
            
            // GB
            if (size > 1073741824) {
                return Math.round(size / 1073741824, 1) + " GB";
            }

            // MB
            if (size > 1048576) {
                return Math.round(size / 1048576, 1) + " MB";
            }

            // KB
            if (size > 1024) {
                return Math.round(size / 1024, 1) + " KB";
            }

            return size + " b";
        },

        /**
         * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields.
         *
         * @method getPos
         * @param {Element} node HTML element or element id to get x, y position from.
         * @param {Element} root Optional root element to stop calculations at.
         * @return {object} Absolute position of the specified element object with x, y fields.
         */
         getPos : function(node, root) {
            var x = 0, y = 0, parent, doc = document, nodeRect, rootRect;

            node = node;
            root = root || doc.body;

            // Returns the x, y cordinate for an element on IE 6 and IE 7
            function getIEPos(node) {
                var bodyElm, rect, x = 0, y = 0;

                if (node) {
                    rect = node.getBoundingClientRect();
                    bodyElm = doc.compatMode === "CSS1Compat" ? doc.documentElement : doc.body;
                    x = rect.left + bodyElm.scrollLeft;
                    y = rect.top + bodyElm.scrollTop;
                }

                return {
                    x : x,
                    y : y
                };
            }

            // Use getBoundingClientRect on IE 6 and IE 7 but not on IE 8 in standards mode
            if (node && node.getBoundingClientRect && ((navigator.userAgent.indexOf('MSIE') > 0) && (doc.documentMode < 8))) {
                nodeRect = getIEPos(node);
                rootRect = getIEPos(root);

                return {
                    x : nodeRect.x - rootRect.x,
                    y : nodeRect.y - rootRect.y
                };
            }

            parent = node;
            while (parent && parent != root && parent.nodeType) {
                x += parent.offsetLeft || 0;
                y += parent.offsetTop || 0;
                parent = parent.offsetParent;
            }

            parent = node.parentNode;
            while (parent && parent != root && parent.nodeType) {
                x -= parent.scrollLeft || 0;
                y -= parent.scrollTop || 0;
                parent = parent.parentNode;
            }

            return {
                x : x,
                y : y
            };
        },

        /**
         * Returns the size of the specified node in pixels.
         *
         * @param {Node} node Node to get the size of.
         * @return {Object} Object with a w and h property.
         */
        getSize : function(node) {
            return {
                w : node.offsetWidth || node.clientWidth,
                h : node.offsetHeight || node.clientHeight
            };
        },

        /**
         * Parses the specified size string into a byte value. For example 10kb becomes 10240.
         *
         * @method parseSize
         * @param {String/Number} size String to parse or number to just pass through.
         * @return {Number} Size in bytes.
         */
        parseSize : function(size) {
            var mul;

            if (typeof(size) == 'string') {
                size = /^([0-9]+)([mgk]?)$/.exec(size.toLowerCase().replace(/[^0-9mkg]/g, ''));
                mul = size[2];
                size = +size[1];

                if (mul == 'g') {
                    size *= 1073741824;
                }

                if (mul == 'm') {
                    size *= 1048576;
                }

                if (mul == 'k') {
                    size *= 1024;
                }
            }

            return size;
        },

        /**
         * Encodes the specified string.
         *
         * @method xmlEncode
         * @param {String} s String to encode.
         * @return {String} Encoded string.
         */
        xmlEncode : function(str) {
            return str ? ('' + str).replace(xmlEncodeRegExp, function(chr) {
                return xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr;
            }) : str;
        },

        /**
         * Forces anything into an array.
         *
         * @method toArray
         * @param {Object} obj Object with length field.
         * @return {Array} Array object containing all items.
         */
        toArray : function(obj) {
            var i, arr = [];

            for (i = 0; i < obj.length; i++) {
                arr[i] = obj[i];
            }

            return arr;
        },
        
        /**
         * Find an element in array and return it's index if present, otherwise return -1.
         *
         * @method inArray
         * @param {mixed} needle Element to find
         * @param {Array} array
         * @return {Int} Index of the element, or -1 if not found
         */
        inArray : function(needle, array) {         
            if (array) {
                if (Array.prototype.indexOf) {
                    return Array.prototype.indexOf.call(array, needle);
                }
            
                for (var i = 0, length = array.length; i < length; i++) {
                    if (array[i] === needle) {
                        return i;
                    }
                }
            }
            return -1;
        },

        /**
         * Extends the language pack object with new items.
         *
         * @param {Object} pack Language pack items to add.
         * @return {Object} Extended language pack object.
         */
        addI18n : function(pack) {
            return plupload.extend(i18n, pack);
        },

        /**
         * Translates the specified string by checking for the english string in the language pack lookup.
         *
         * @param {String} str String to look for.
         * @return {String} Translated string or the input string if it wasn't found.
         */
        translate : function(str) {
            return i18n[str] || str;
        },
        
        /**
         * Checks if object is empty.
         *
         * @param {Object} obj Object to check.
         * @return {Boolean}
         */
        isEmptyObj : function(obj) {
            if (obj === undef) return true;
            
            for (var prop in obj) {
                return false;   
            }
            return true;
        },
        
        /**
         * Checks if specified DOM element has specified class.
         *
         * @param {Object} obj DOM element like object to add handler to.
         * @param {String} name Class name
         */
        hasClass : function(obj, name) {
            var regExp;
        
            if (obj.className == '') {
                return false;
            }

            regExp = new RegExp("(^|\\s+)"+name+"(\\s+|$)");

            return regExp.test(obj.className);
        },
        
        /**
         * Adds specified className to specified DOM element.
         *
         * @param {Object} obj DOM element like object to add handler to.
         * @param {String} name Class name
         */
        addClass : function(obj, name) {
            if (!plupload.hasClass(obj, name)) {
                obj.className = obj.className == '' ? name : obj.className.replace(/\s+$/, '')+' '+name;
            }
        },
        
        /**
         * Removes specified className from specified DOM element.
         *
         * @param {Object} obj DOM element like object to add handler to.
         * @param {String} name Class name
         */
        removeClass : function(obj, name) {
            var regExp = new RegExp("(^|\\s+)"+name+"(\\s+|$)");
            
            obj.className = obj.className.replace(regExp, function($0, $1, $2) {
                return $1 === ' ' && $2 === ' ' ? ' ' : '';
            });
        },
    
        /**
         * Returns a given computed style of a DOM element.
         *
         * @param {Object} obj DOM element like object.
         * @param {String} name Style you want to get from the DOM element
         */
        getStyle : function(obj, name) {
            if (obj.currentStyle) {
                return obj.currentStyle[name];
            } else if (window.getComputedStyle) {
                return window.getComputedStyle(obj, null)[name];
            }
        },

        /**
         * Adds an event handler to the specified object and store reference to the handler
         * in objects internal Plupload registry (@see removeEvent).
         *
         * @param {Object} obj DOM element like object to add handler to.
         * @param {String} name Name to add event listener to.
         * @param {Function} callback Function to call when event occurs.
         * @param {String} (optional) key that might be used to add specifity to the event record.
         */
        addEvent : function(obj, name, callback) {
            var func, events, types, key;
            
            // if passed in, event will be locked with this key - one would need to provide it to removeEvent
            key = arguments[3];
                        
            name = name.toLowerCase();
                        
            // Initialize unique identifier if needed
            if (uid === undef) {
                uid = 'Plupload_' + plupload.guid();
            }

            // Add event listener
            if (obj.addEventListener) {
                func = callback;
                
                obj.addEventListener(name, func, false);
            } else if (obj.attachEvent) {
                
                func = function() {
                    var evt = window.event;

                    if (!evt.target) {
                        evt.target = evt.srcElement;
                    }

                    evt.preventDefault = preventDefault;
                    evt.stopPropagation = stopPropagation;

                    callback(evt);
                };
                obj.attachEvent('on' + name, func);
            } 
            
            // Log event handler to objects internal Plupload registry
            if (obj[uid] === undef) {
                obj[uid] = plupload.guid();
            }
            
            if (!eventhash.hasOwnProperty(obj[uid])) {
                eventhash[obj[uid]] = {};
            }
            
            events = eventhash[obj[uid]];
            
            if (!events.hasOwnProperty(name)) {
                events[name] = [];
            }
                    
            events[name].push({
                func: func,
                orig: callback, // store original callback for IE
                key: key
            });
        },
        
        
        /**
         * Remove event handler from the specified object. If third argument (callback)
         * is not specified remove all events with the specified name.
         *
         * @param {Object} obj DOM element to remove event listener(s) from.
         * @param {String} name Name of event listener to remove.
         * @param {Function|String} (optional) might be a callback or unique key to match.
         */
        removeEvent: function(obj, name) {
            var type, callback, key;
            
            // match the handler either by callback or by key   
            if (typeof(arguments[2]) == "function") {
                callback = arguments[2];
            } else {
                key = arguments[2];
            }
                        
            name = name.toLowerCase();
            
            if (obj[uid] && eventhash[obj[uid]] && eventhash[obj[uid]][name]) {
                type = eventhash[obj[uid]][name];
            } else {
                return;
            }
            
                
            for (var i=type.length-1; i>=0; i--) {
                // undefined or not, key should match           
                if (type[i].key === key || type[i].orig === callback) {
                                        
                    if (obj.removeEventListener) {
                        obj.removeEventListener(name, type[i].func, false);     
                    } else if (obj.detachEvent) {
                        obj.detachEvent('on'+name, type[i].func);
                    }
                    
                    type[i].orig = null;
                    type[i].func = null;
                    
                    type.splice(i, 1);
                    
                    // If callback was passed we are done here, otherwise proceed
                    if (callback !== undef) {
                        break;
                    }
                }           
            }   
            
            // If event array got empty, remove it
            if (!type.length) {
                delete eventhash[obj[uid]][name];
            }
            
            // If Plupload registry has become empty, remove it
            if (plupload.isEmptyObj(eventhash[obj[uid]])) {
                delete eventhash[obj[uid]];
                
                // IE doesn't let you remove DOM object property with - delete
                try {
                    delete obj[uid];
                } catch(e) {
                    obj[uid] = undef;
                }
            }
        },
        
        
        /**
         * Remove all kind of events from the specified object
         *
         * @param {Object} obj DOM element to remove event listeners from.
         * @param {String} (optional) unique key to match, when removing events.
         */
        removeAllEvents: function(obj) {
            var key = arguments[1];
            
            if (obj[uid] === undef || !obj[uid]) {
                return;
            }
            
            plupload.each(eventhash[obj[uid]], function(events, name) {
                plupload.removeEvent(obj, name, key);
            });     
        }
    };
    

    /**
     * Uploader class, an instance of this class will be created for each upload field.
     *
     * @example
     * var uploader = new plupload.Uploader({
     *     runtimes : 'gears,html5,flash',
     *     browse_button : 'button_id'
     * });
     *
     * uploader.bind('Init', function(up) {
     *     alert('Supports drag/drop: ' + (!!up.features.dragdrop));
     * });
     *
     * uploader.bind('FilesAdded', function(up, files) {
     *     alert('Selected files: ' + files.length);
     * });
     *
     * uploader.bind('QueueChanged', function(up) {
     *     alert('Queued files: ' + uploader.files.length);
     * });
     *
     * uploader.init();
     *
     * @class plupload.Uploader
     */

    /**
     * Constructs a new uploader instance.
     *
     * @constructor
     * @method Uploader
     * @param {Object} settings Initialization settings, to be used by the uploader instance and runtimes.
     */
    plupload.Uploader = function(settings) {
        var events = {}, total, files = [], startTime, disabled = false;

        // Inital total state
        total = new plupload.QueueProgress();

        // Default settings
        settings = plupload.extend({
            chunk_size : 0,
            multipart : true,
            multi_selection : true,
            file_data_name : 'file',
            filters : []
        }, settings);

        // Private methods
        function uploadNext() {
            var file, count = 0, i;

            if (this.state == plupload.STARTED) {
                // Find first QUEUED file
                for (i = 0; i < files.length; i++) {
                    if (!file && files[i].status == plupload.QUEUED) {
                        file = files[i];
                        file.status = plupload.UPLOADING;
                        if (this.trigger("BeforeUpload", file)) {
                            this.trigger("UploadFile", file);
                        }
                    } else {
                        count++;
                    }
                }

                // All files are DONE or FAILED
                if (count == files.length) {
                    this.stop();
                    this.trigger("UploadComplete", files);
                }
            }
        }

        function calc() {
            var i, file;

            // Reset stats
            total.reset();

            // Check status, size, loaded etc on all files
            for (i = 0; i < files.length; i++) {
                file = files[i];

                if (file.size !== undef) {
                    total.size += file.size;
                    total.loaded += file.loaded;
                } else {
                    total.size = undef;
                }

                if (file.status == plupload.DONE) {
                    total.uploaded++;
                } else if (file.status == plupload.FAILED) {
                    total.failed++;
                } else {
                    total.queued++;
                }
            }

            // If we couldn't calculate a total file size then use the number of files to calc percent
            if (total.size === undef) {
                total.percent = files.length > 0 ? Math.ceil(total.uploaded / files.length * 100) : 0;
            } else {
                total.bytesPerSec = Math.ceil(total.loaded / ((+new Date() - startTime || 1) / 1000.0));
                total.percent = total.size > 0 ? Math.ceil(total.loaded / total.size * 100) : 0;
            }
        }

        // Add public methods
        plupload.extend(this, {
            /**
             * Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED.
             * These states are controlled by the stop/start methods. The default value is STOPPED.
             *
             * @property state
             * @type Number
             */
            state : plupload.STOPPED,
            
            /**
             * Current runtime name.
             *
             * @property runtime
             * @type String
             */
            runtime: '',

            /**
             * Map of features that are available for the uploader runtime. Features will be filled
             * before the init event is called, these features can then be used to alter the UI for the end user.
             * Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize.
             *
             * @property features
             * @type Object
             */
            features : {},

            /**
             * Current upload queue, an array of File instances.
             *
             * @property files
             * @type Array
             * @see plupload.File
             */
            files : files,

            /**
             * Object with name/value settings.
             *
             * @property settings
             * @type Object
             */
            settings : settings,

            /**
             * Total progess information. How many files has been uploaded, total percent etc.
             *
             * @property total
             * @type plupload.QueueProgress
             */
            total : total,

            /**
             * Unique id for the Uploader instance.
             *
             * @property id
             * @type String
             */
            id : plupload.guid(),

            /**
             * Initializes the Uploader instance and adds internal event listeners.
             *
             * @method init
             */
            init : function() {
                var self = this, i, runtimeList, a, runTimeIndex = 0, items;

                if (typeof(settings.preinit) == "function") {
                    settings.preinit(self);
                } else {
                    plupload.each(settings.preinit, function(func, name) {
                        self.bind(name, func);
                    });
                }

                settings.page_url = settings.page_url || document.location.pathname.replace(/\/[^\/]+$/g, '/');

                // If url is relative force it absolute to the current page
                if (!/^(\w+:\/\/|\/)/.test(settings.url)) {
                    settings.url = settings.page_url + settings.url;
                }

                // Convert settings
                settings.chunk_size = plupload.parseSize(settings.chunk_size);
                settings.max_file_size = plupload.parseSize(settings.max_file_size);

                // Add files to queue
                self.bind('FilesAdded', function(up, selected_files) {
                    var i, file, count = 0, extensionsRegExp, filters = settings.filters;

                    // Convert extensions to regexp
                    if (filters && filters.length) {
                        extensionsRegExp = [];
                        
                        plupload.each(filters, function(filter) {
                            plupload.each(filter.extensions.split(/,/), function(ext) {
                                if (/^\s*\*\s*$/.test(ext)) {
                                    extensionsRegExp.push('\\.*');
                                } else {
                                    extensionsRegExp.push('\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\'.replace(/./g, '\\$&')) + ']', 'g'), '\\$&'));
                                }
                            });
                        });
                        
                        extensionsRegExp = new RegExp(extensionsRegExp.join('|') + '$', 'i');
                    }

                    for (i = 0; i < selected_files.length; i++) {
                        file = selected_files[i];
                        file.loaded = 0;
                        file.percent = 0;
                        file.status = plupload.QUEUED;

                        // Invalid file extension
                        if (extensionsRegExp && !extensionsRegExp.test(file.name)) {
                            up.trigger('Error', {
                                code : plupload.FILE_EXTENSION_ERROR,
                                message : plupload.translate('File extension error.'),
                                file : file
                            });

                            continue;
                        }

                        // Invalid file size
                        if (file.size !== undef && file.size > settings.max_file_size) {
                            up.trigger('Error', {
                                code : plupload.FILE_SIZE_ERROR,
                                message : plupload.translate('File size error.'),
                                file : file
                            });

                            continue;
                        }

                        // Add valid file to list
                        files.push(file);
                        count++;
                    }

                    // Only trigger QueueChanged event if any files where added
                    if (count) {
                        delay(function() {
                            self.trigger("QueueChanged");
                            self.refresh();
                        }, 1);
                    } else {
                        return false; // Stop the FilesAdded event from immediate propagation
                    }
                });

                // Generate unique target filenames
                if (settings.unique_names) {
                    self.bind("UploadFile", function(up, file) {
                        var matches = file.name.match(/\.([^.]+)$/), ext = "tmp";

                        if (matches) {
                            ext = matches[1];
                        }

                        file.target_name = file.id + '.' + ext;
                    });
                }

                self.bind('UploadProgress', function(up, file) {
                    file.percent = file.size > 0 ? Math.ceil(file.loaded / file.size * 100) : 100;
                    calc();
                });

                self.bind('StateChanged', function(up) {
                    if (up.state == plupload.STARTED) {
                        // Get start time to calculate bps
                        startTime = (+new Date());
                        
                    } else if (up.state == plupload.STOPPED) {                      
                        // Reset currently uploading files
                        for (i = up.files.length - 1; i >= 0; i--) {
                            if (up.files[i].status == plupload.UPLOADING) {
                                up.files[i].status = plupload.QUEUED;
                                calc();
                            }
                        }
                    }
                });

                self.bind('QueueChanged', calc);

                self.bind("Error", function(up, err) {
                    // Set failed status if an error occured on a file
                    if (err.file) {
                        err.file.status = plupload.FAILED;
                        calc();

                        // Upload next file but detach it from the error event
                        // since other custom listeners might want to stop the queue
                        if (up.state == plupload.STARTED) {
                            delay(function() {
                                uploadNext.call(self);
                            }, 1);
                        }
                    }
                });

                self.bind("FileUploaded", function(up, file) {
                    file.status = plupload.DONE;
                    file.loaded = file.size;
                    up.trigger('UploadProgress', file);

                    // Upload next file but detach it from the error event
                    // since other custom listeners might want to stop the queue
                    delay(function() {
                        uploadNext.call(self);
                    }, 1);
                });

                // Setup runtimeList
                if (settings.runtimes) {
                    runtimeList = [];
                    items = settings.runtimes.split(/\s?,\s?/);

                    for (i = 0; i < items.length; i++) {
                        if (runtimes[items[i]]) {
                            runtimeList.push(runtimes[items[i]]);
                        }
                    }
                } else {
                    runtimeList = runtimes;
                }

                // Call init on each runtime in sequence
                function callNextInit() {
                    var runtime = runtimeList[runTimeIndex++], features, requiredFeatures, i;

                    if (runtime) {
                        features = runtime.getFeatures();

                        // Check if runtime supports required features
                        requiredFeatures = self.settings.required_features;
                        if (requiredFeatures) {
                            requiredFeatures = requiredFeatures.split(',');

                            for (i = 0; i < requiredFeatures.length; i++) {
                                // Specified feature doesn't exist
                                if (!features[requiredFeatures[i]]) {
                                    callNextInit();
                                    return;
                                }
                            }
                        }

                        // Try initializing the runtime
                        runtime.init(self, function(res) {
                            if (res && res.success) {
                                // Successful initialization
                                self.features = features;
                                self.runtime = runtime.name;
                                self.trigger('Init', {runtime : runtime.name});
                                self.trigger('PostInit');
                                self.refresh();
                            } else {
                                callNextInit();
                            }
                        });
                    } else {
                        // Trigger an init error if we run out of runtimes
                        self.trigger('Error', {
                            code : plupload.INIT_ERROR,
                            message : plupload.translate('Init error.')
                        });
                    }
                }

                callNextInit();

                if (typeof(settings.init) == "function") {
                    settings.init(self);
                } else {
                    plupload.each(settings.init, function(func, name) {
                        self.bind(name, func);
                    });
                }
            },

            /**
             * Refreshes the upload instance by dispatching out a refresh event to all runtimes.
             * This would for example reposition flash/silverlight shims on the page.
             *
             * @method refresh
             */
            refresh : function() {
                this.trigger("Refresh");
            },

            /**
             * Starts uploading the queued files.
             *
             * @method start
             */
            start : function() {
                if (files.length && this.state != plupload.STARTED) {
                    this.state = plupload.STARTED;
                    this.trigger("StateChanged");   
                    
                    uploadNext.call(this);              
                }
            },

            /**
             * Stops the upload of the queued files.
             *
             * @method stop
             */
            stop : function() {
                if (this.state != plupload.STOPPED) {
                    this.state = plupload.STOPPED;  
                    this.trigger("CancelUpload");               
                    this.trigger("StateChanged");
                }
            },
            
            /** 
             * Disables/enables browse button on request.
             *
             * @method disableBrowse
             * @param {Boolean} disable Whether to disable or enable (default: true)
             */
            disableBrowse : function() {
                disabled = arguments[0] !== undef ? arguments[0] : true;
                this.trigger("DisableBrowse", disabled);
            },

            /**
             * Returns the specified file object by id.
             *
             * @method getFile
             * @param {String} id File id to look for.
             * @return {plupload.File} File object or undefined if it wasn't found;
             */
            getFile : function(id) {
                var i;

                for (i = files.length - 1; i >= 0; i--) {
                    if (files[i].id === id) {
                        return files[i];
                    }
                }
            },

            /**
             * Removes a specific file.
             *
             * @method removeFile
             * @param {plupload.File} file File to remove from queue.
             */
            removeFile : function(file) {
                var i;

                for (i = files.length - 1; i >= 0; i--) {
                    if (files[i].id === file.id) {
                        return this.splice(i, 1)[0];
                    }
                }
            },

            /**
             * Removes part of the queue and returns the files removed. This will also trigger the FilesRemoved and QueueChanged events.
             *
             * @method splice
             * @param {Number} start (Optional) Start index to remove from.
             * @param {Number} length (Optional) Lengh of items to remove.
             * @return {Array} Array of files that was removed.
             */
            splice : function(start, length) {
                var removed;

                // Splice and trigger events
                removed = files.splice(start === undef ? 0 : start, length === undef ? files.length : length);

                this.trigger("FilesRemoved", removed);
                this.trigger("QueueChanged");

                return removed;
            },

            /**
             * Dispatches the specified event name and it's arguments to all listeners.
             *
             *
             * @method trigger
             * @param {String} name Event name to fire.
             * @param {Object..} Multiple arguments to pass along to the listener functions.
             */
            trigger : function(name) {
                var list = events[name.toLowerCase()], i, args;

                // console.log(name, arguments);

                if (list) {
                    // Replace name with sender in args
                    args = Array.prototype.slice.call(arguments);
                    args[0] = this;

                    // Dispatch event to all listeners
                    for (i = 0; i < list.length; i++) {
                        // Fire event, break chain if false is returned
                        if (list[i].func.apply(list[i].scope, args) === false) {
                            return false;
                        }
                    }
                }

                return true;
            },
            
            /**
             * Check whether uploader has any listeners to the specified event.
             *
             * @method hasEventListener
             * @param {String} name Event name to check for.
             */
            hasEventListener : function(name) {
                return !!events[name.toLowerCase()];
            },

            /**
             * Adds an event listener by name.
             *
             * @method bind
             * @param {String} name Event name to listen for.
             * @param {function} func Function to call ones the event gets fired.
             * @param {Object} scope Optional scope to execute the specified function in.
             */
            bind : function(name, func, scope) {
                var list;

                name = name.toLowerCase();
                list = events[name] || [];
                list.push({func : func, scope : scope || this});
                events[name] = list;
            },

            /**
             * Removes the specified event listener.
             *
             * @method unbind
             * @param {String} name Name of event to remove.
             * @param {function} func Function to remove from listener.
             */
            unbind : function(name) {
                name = name.toLowerCase();

                var list = events[name], i, func = arguments[1];

                if (list) {
                    if (func !== undef) {
                        for (i = list.length - 1; i >= 0; i--) {
                            if (list[i].func === func) {
                                list.splice(i, 1);
                                    break;
                            }
                        }
                    } else {
                        list = [];
                    }

                    // delete event list if it has become empty
                    if (!list.length) {
                        delete events[name];
                    }
                }
            },

            /**
             * Removes all event listeners.
             *
             * @method unbindAll
             */
            unbindAll : function() {
                var self = this;
                
                plupload.each(events, function(list, name) {
                    self.unbind(name);
                });
            },
            
            /**
             * Destroys Plupload instance and cleans after itself.
             *
             * @method destroy
             */
            destroy : function() {  
                this.stop();                        
                this.trigger('Destroy');
                
                // Clean-up after uploader itself
                this.unbindAll();
            }

            /**
             * Fires when the current RunTime has been initialized.
             *
             * @event Init
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             */

            /**
             * Fires after the init event incase you need to perform actions there.
             *
             * @event PostInit
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             */

            /**
             * Fires when the silverlight/flash or other shim needs to move.
             *
             * @event Refresh
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             */
    
            /**
             * Fires when the overall state is being changed for the upload queue.
             *
             * @event StateChanged
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             */

            /**
             * Fires when a file is to be uploaded by the runtime.
             *
             * @event UploadFile
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {plupload.File} file File to be uploaded.
             */

            /**
             * Fires when just before a file is uploaded. This event enables you to override settings
             * on the uploader instance before the file is uploaded.
             *
             * @event BeforeUpload
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {plupload.File} file File to be uploaded.
             */

            /**
             * Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance.
             *
             * @event QueueChanged
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             */
    
            /**
             * Fires while a file is being uploaded. Use this event to update the current file upload progress.
             *
             * @event UploadProgress
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {plupload.File} file File that is currently being uploaded.
             */

            /**
             * Fires while a file was removed from queue.
             *
             * @event FilesRemoved
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {Array} files Array of files that got removed.
             */

            /**
             * Fires while when the user selects files to upload.
             *
             * @event FilesAdded
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {Array} files Array of file objects that was added to queue/selected by the user.
             */

            /**
             * Fires when a file is successfully uploaded.
             *
             * @event FileUploaded
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {plupload.File} file File that was uploaded.
             * @param {Object} response Object with response properties.
             */

            /**
             * Fires when file chunk is uploaded.
             *
             * @event ChunkUploaded
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {plupload.File} file File that the chunk was uploaded for.
             * @param {Object} response Object with response properties.
             */

            /**
             * Fires when all files in a queue are uploaded.
             *
             * @event UploadComplete
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {Array} files Array of file objects that was added to queue/selected by the user.
             */

            /**
             * Fires when a error occurs.
             *
             * @event Error
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             * @param {Object} error Contains code, message and sometimes file and other details.
             */
             
             /**
             * Fires when destroy method is called.
             *
             * @event Destroy
             * @param {plupload.Uploader} uploader Uploader instance sending the event.
             */
        });
    };

    /**
     * File instance.
     *
     * @class plupload.File
     * @param {String} name Name of the file.
     * @param {Number} size File size.
     */

    /**
     * Constructs a new file instance.
     *
     * @constructor
     * @method File
     * @param {String} id Unique file id.
     * @param {String} name File name.
     * @param {Number} size File size in bytes.
     */
    plupload.File = function(id, name, size) {
        var self = this; // Setup alias for self to reduce code size when it's compressed

        /**
         * File id this is a globally unique id for the specific file.
         *
         * @property id
         * @type String
         */
        self.id = id;

        /**
         * File name for example "myfile.gif".
         *
         * @property name
         * @type String
         */
        self.name = name;

        /**
         * File size in bytes.
         *
         * @property size
         * @type Number
         */
        self.size = size;

        /**
         * Number of bytes uploaded of the files total size.
         *
         * @property loaded
         * @type Number
         */
        self.loaded = 0;

        /**
         * Number of percentage uploaded of the file.
         *
         * @property percent
         * @type Number
         */
        self.percent = 0;

        /**
         * Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE.
         *
         * @property status
         * @type Number
         * @see plupload
         */
        self.status = 0;
    };

    /**
     * Runtime class gets implemented by each upload runtime.
     *
     * @class plupload.Runtime
     * @static
     */
    plupload.Runtime = function() {
        /**
         * Returns a list of supported features for the runtime.
         *
         * @return {Object} Name/value object with supported features.
         */
        this.getFeatures = function() {
        };

        /**
         * Initializes the upload runtime. This method should add necessary items to the DOM and register events needed for operation. 
         *
         * @method init
         * @param {plupload.Uploader} uploader Uploader instance that needs to be initialized.
         * @param {function} callback Callback function to execute when the runtime initializes or fails to initialize. If it succeeds an object with a parameter name success will be set to true.
         */
        this.init = function(uploader, callback) {
        };
    };

    /**
     * Runtime class gets implemented by each upload runtime.
     *
     * @class plupload.QueueProgress
     */

    /**
     * Constructs a queue progress.
     *
     * @constructor
     * @method QueueProgress
     */
     plupload.QueueProgress = function() {
        var self = this; // Setup alias for self to reduce code size when it's compressed

        /**
         * Total queue file size.
         *
         * @property size
         * @type Number
         */
        self.size = 0;

        /**
         * Total bytes uploaded.
         *
         * @property loaded
         * @type Number
         */
        self.loaded = 0;

        /**
         * Number of files uploaded.
         *
         * @property uploaded
         * @type Number
         */
        self.uploaded = 0;

        /**
         * Number of files failed to upload.
         *
         * @property failed
         * @type Number
         */
        self.failed = 0;

        /**
         * Number of files yet to be uploaded.
         *
         * @property queued
         * @type Number
         */
        self.queued = 0;

        /**
         * Total percent of the uploaded bytes.
         *
         * @property percent
         * @type Number
         */
        self.percent = 0;

        /**
         * Bytes uploaded per second.
         *
         * @property bytesPerSec
         * @type Number
         */
        self.bytesPerSec = 0;

        /**
         * Resets the progress to it's initial values.
         *
         * @method reset
         */
        self.reset = function() {
            self.size = self.loaded = self.uploaded = self.failed = self.queued = self.percent = self.bytesPerSec = 0;
        };
    };

    // Create runtimes namespace
    plupload.runtimes = {};

    // Expose plupload namespace
    window.plupload = plupload;
})();

});